跳到主要内容

SpringCloud 微服务网关和JWT令牌

本篇笔记是学习畅购商城的,微服务网关那里

微服务搭建

一个项目中可能会用到不止一个网关,所以我们将网关微服务放在 changgou-gateway 父工程下。有些依赖是所有网关微服务都要用到的,所以将这些依赖放在父工程下:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

现在我们创建一个名为 changou-gateway-web 的微服务在 changgou-gateway 父工程下

启动类和配置文件不能少

@SpringBootApplication
@EnableEurekaClient
public class GatewayWebApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayWebApplication.class, args);
}
}

配置文件

spring:
application:
name: gateway-web
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
server:
port: 8001
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
management:
endpoint:
gateway:
enabled: true
web:
exposure:
include: true

网关路由配置

这里只记录几个常用的路由配置,细节看 SpringCloudGateway 那篇笔记

Host 路由

# 用户请求的域名规格配置,所有以robod.changgou.com开头的请求都将被路由到http://localhost:18081微服务
# 例如 http://robod.changgou.com:8001/brand ——> http://localhost:18081/brand
# 但是首先得在hosts文件中配置一下: 127.0.0.1 robod.changgou.com
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route # 唯一标识符
uri: http://localhost:18081
predicates:
- Host=robod.changgou.com**

Path 路径匹配过滤配置

# 所有以/brand开头的请求都将路由到http://localhost:18081
# 例如 localhost:8001/brand ——> localhost:18081/brand
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/brand/**

PrefixPath 过滤配置(自动加前缀)

# 自动加上某个前缀,用户请求/** ——>/brand/**
# 例如 localhost:8001/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/**
filters:
- PrefixPath=/brand

StripPrefix 过滤配置(去掉路径)

这个与上相反,自动去掉某几个路径

# 将请求路径中的前n个路径去掉,请求路径以/区分,一个/代表一个路径
# 例如 localhost:8001/api/brand/111 ——> localhost:8001/brand/111 ——> localhost:18081/brand/111
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: http://localhost:18081
predicates:
- Path=/**
filters:
- StripPrefix=1

LoadBalancerClient 路由过滤器(客户端负载均衡)

# 使用LoadBalancerClient实现负载均衡,后面的goods是微服务的名称,主要应用于集群环境
# 比如现在有5台服务器都是goods微服务,网关就会自动将请求发送给不同的服务器达到负载均衡的目的
spring:
cloud:
gateway:
routes:
- id: changgou_goods_route
uri: lb://goods

网关限流

当访问量多大的时候,我们的服务就可能会挂掉,所以我们需要对每个微服务进行限流,但是这样比较麻烦。有了网关之后,我们可以对网关进行限流,因为所有的请求必须通过网关才能到达微服务,这样比较方便。

令牌桶算法

常见的限流算法有计数器,漏斗,令牌桶算法。令牌桶算法有以下几个特点:

  • 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
  • 根据限流大小,设置按照一定的速率往桶里添加令牌;
  • 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
  • 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
  • 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

使用令牌桶进行请求次数限流

Spring cloud gateway 默认使用 Redis 的 RateLimter 限流算法来实现。首先在 changgou-gateway-web 中添加 Redis 的依赖:

注意:Redis 要放在子模块里面它才会自动配置

<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

然后我们需要有限流的 Key,这里用 IP 来当作限流的 Key,限制某一个 IP 在一定时间段的访问次数,在启动类中定义一个 Bean 用于获取 key:

@Bean(name = "ipKeyResolver")
public KeyResolver userKeyResolver() {
// 这里使用 Lambda 重写这个接口
return exchange -> {
String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName();
return Mono.just(ip);
};
}

这个 KeyResolver 就是 Gateway 提供的接口

public interface KeyResolver {

Mono<String> resolve(ServerWebExchange exchange);

}

接下来还得在配置文件中配置一下:

spring:
application:
name: gateway-web
cloud:
gateway:
routes:
- id: goodserver # 唯一标识符
uri: http://localhost:18081 # 注意,这里不能写自己这个网关服务(写了也没卵用)
predicates: # 编写路由断言
- Path=/spu/goods/** # 匹配这个 /spu/goods/ 路径
filters:
- name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的factory
args:
# 用户身份唯一标识符
key-resolver: "#{@ipKeyResolver}"
# 允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的容量,允许在一秒钟内完成的最大请求数
redis-rate-limiter.burstCapacity: 1

既然是使用 Redis 的 RateLimter 限流算法,那么 Redis 的配置自然不能少。

#Redis配置
spring:
application:
redis:
host: 192.168.211.132
port: 6379

限流的配置就配置好了,现在如果在1秒内请求超过1次的话就会被拒绝。

JWT 的使用

先来回顾一下 JWT 的使用

导入依赖:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

创建 Token

下面回顾一下如何使用:

public class JWTUtil {
public String createToken() {
JwtBuilder builder = Jwts.builder()
.setId("test1")
.setSubject("主题")
.setAudience("张三")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"this is signing key");

Map<String,Object> map = new HashMap<>();
map.put("name","alsritter");
builder.addClaims(map);
return builder.compact();
}

public static void main(String[] args) {
System.out.println(new JWTUtil().createToken());
// output:eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ0ZXN0MSIsInN1YiI6IuS4u-mimCIsImF1ZCI6IuW8oOS4iSIsImlhdCI6MTYyMTY1MTM1NywibmFtZSI6ImFsc3JpdHRlciJ9.2Z6G8OGeM_0mK7D_6ZDN3lVYabveEHNAftP017imlK8
}
}

解析 Token

public String parseToken() {
String compactJwt="eyJhbGciOiJIUzI1NiJ9" +
".eyJqdGkiOiJ0ZXN0MSIsInN1YiI6IuS4u-mimCIsImF1ZCI6IuW8oOS4iSIsImlhdCI6MTYyMTY1MTM1NywibmFtZSI6ImFsc3JpdHRlciJ9" +
".2Z6G8OGeM_0mK7D_6ZDN3lVYabveEHNAftP017imlK8";
Claims claims = Jwts.parser().
setSigningKey("this is signing key").
parseClaimsJws(compactJwt).
getBody();
return claims.toString();
}

查看输出:

用户登录与鉴权

用JWT实现用户登录与鉴权,流程如下:

首先我们需要准备一个 JWT 的工具类,JWTUtil,放在 changgou-common 下:

public class JwtUtil {
/**
* 有效期,一个小时
*/
public static final Long JWT_TTL = 3600000L;

/**
* Jwt令牌信息
*/
public static final String JWT_KEY = "RobodLee";

/**
* 密钥
*/
public static SecretKey secretKey = generalKey();

/**
* 生成令牌
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
//指定算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

//当前系统时间
long nowMillis = System.currentTimeMillis();
//令牌签发时间
Date now = new Date(nowMillis);

//如果令牌有效期为null,则默认设置有效期1小时
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}

//令牌过期时间设置
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);

//生成秘钥
//SecretKey secretKey = generalKey();

//封装Jwt令牌信息
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("alsritter") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm,secretKey) // 签名算法以及密匙
.setExpiration(expDate); // 设置过期时间
return builder.compact();
}

/**
* 生成加密 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}

/**
* 解析令牌数据
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
//SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}

然后创建一个用户微服务 changou-service-user,在 UserController 中编写登录逻辑

在这段代码中,调用 Service 层从数据库中查出对应的 User,然后比对 password,看密码是否正确。如果正确,就调用 JwtUtil 创建一个 JWT 令牌,并放入一些简单的信息。

public Result<String> login(String username, String password, HttpServletResponse response) {
User user = userService.findById(username);

if (BCrypt.checkpw(password, user.getPassword())){
Map<String,Object> tokenInfo = new HashMap<>(4);
tokenInfo.put("role","USER");
tokenInfo.put("success","SUCCESS");
tokenInfo.put("username",username);
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(tokenInfo), null);
Cookie cookie = new Cookie("Authorization",token);
cookie.setDomain("localhost");
cookie.setPath("/");
response.addCookie(cookie);
return new Result<>(true, StatusCode.OK,"登录成功",token);
}

return new Result<>(false,StatusCode.LOGIN_ERROR,"登录失败");
}

然后将 JWT 令牌存入 Cookie 中,并返回给前端。如果登录失败就返回登录失败的信息。

然后就是在网关微服务中添加相应的逻辑了,在 changgou-gateway-web 中配置一下,配置一下 User 微服务的路由。

spring:
application:
name: gateway-web
cloud:
gateway:
routes:
- id: changgou_user_route # 唯一标识符(随便写,不要和别的名字冲突就行)
uri: http://localhost:18088
predicates:
- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1

再在网关添加一个过滤器:

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

private static final String AUTHORIZE_TOKEN = "Authorization";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String token;
//从头中获取Token
token = request.getHeaders().getFirst(AUTHORIZE_TOKEN);
//请求头中没有Token就从参数中获取
if (StringUtils.isEmpty(token)){
token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
}

//参数中再没有Token就从Cookie中获取
if (StringUtils.isEmpty(token)){
HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
if (cookie!=null){
token = cookie.getValue();
}
}

//还是没有Token就拦截
if (StringUtils.isEmpty(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

//Token不为空就校验Token
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
//报异常说明Token是错误的,拦截
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

这段代码就是分别从 Header,参数,Cookie 中看有没有 Token 信息,没有的话就说明用户没有权限,拦截下来。有 Token 的话就解析一下 Token 有没有错,错误就拦截下来。如果都没有问题的话就放行,将请求路由到用户微服务中。

这是没有Token的情况下👆

当我们登陆后就会获取到Token👇

当我们携带着 token 去访问就没有问题了👇